/*
    This file is part of QTau
    Copyright (C) 2013-2018  Tobias "Tomoko" Platen <tplaten@posteo.de>
    Copyright (C) 2013       digited       <https://github.com/digited>
    Copyright (C) 2010-2013  HAL@ShurabaP  <https://github.com/haruneko>

    QTau is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    SPDX-License-Identifier: GPL-3.0+
*/

// [a] from Hatsune Miku, estimated using praat
// used to configure EpR voice model, see http://www.mtg.upf.edu/node/1231

float init_f[6] = {915.597, 1408.48, 1892.801, 4025.529, 4509.241, 5500};
float init_bw[6] = {174.803, 167.995, 125.245, 411.338, 429.417, 0};
float init_gain_db[6] = {15, 15, 10, 5, 5, 2};

float init_controls[3] = {22.5, -7E-5, 77};

#include <sekai/midi.h>

/* --------------------------------------------------------------------------- *
 *
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* --------------------------------------------------------------------------- */

#define __devloglevel__ 20

#include <unistd.h>
#include <QAction>
#include <QTimer>
#include <QtConcurrent/QtConcurrent>
#include "madde_synth.h"
#include "maddefile.h"

QString MaddeLOIDSynth::name() { return "MaddeLOID"; }
QString MaddeLOIDSynth::description() {
  return "An additive singing synthesizer";
}
QString MaddeLOIDSynth::version() { return "18.04"; }

#include <math.h>
#include "maddewindow.h"

void MaddeLOIDSynth::start(float keyNum) {
  if (_threadRunning) return;

  _f0 = frequencyFromNote(keyNum);
  _phase = 0;

  // _ctrl->startThread(this);
  QtConcurrent::run(this, &MaddeLOIDSynth::run);
}
void MaddeLOIDSynth::stop() { _f0 = 0; }

int MaddeLOIDSynth::run() {
  _threadRunning = true;
  while (_f0) {
    while (jack_ringbuffer_write_space(_ringbuffer) <
           FFT_SIZE / 2 * sizeof(float)) {
      usleep(1000);
      if (_f0 == 0) break;
    }
    if (_f0) {
      int nharmonics = 50;

      for (int i = 0; i < 6; i++) {
        if (_resNeedsUpdate[i]) {
          EprResonanceUpdate(&_res[i], _osc.fs);
          _resNeedsUpdate[i] = false;
        }
      }

      for (int i = 0; i < 50; i++) {
        if (_f0 > 0) {
          float factor = 0.1;
          float f = _f0 * (i + 1);
          double gain = EprAtFrequency(&_src, f, _osc.fs, _res, RES_COUNT);
          _osc.amp[i] =
              pow(M_E, (gain / TWENTY_OVER_LOG10)) / nharmonics * factor;
          _osc.frq[i] = f;
        } else {
          _osc.amp[i] = 0;
          _osc.frq[i] = 0;
        }
      }

      _osc.processOneFrame();

      float output_buffer[FFT_SIZE / 2];
      for (int i = 0; i < FFT_SIZE / 2; i++) {
        output_buffer[i] = _osc.output_buffer[i] * 0.9;
      }
      jack_ringbuffer_write(_ringbuffer, (char*)output_buffer,
                            FFT_SIZE / 2 * sizeof(float));
    };
  }
  _threadRunning = false;
  return 0;
}

void MaddeLOIDSynth::readData(float* data, int dataLength) {
  unsigned int dataLength2 = dataLength * sizeof(float);
  unsigned int readSpace = jack_ringbuffer_read_space(_ringbuffer);

  readSpace = jack_ringbuffer_read_space(_ringbuffer);

  if (readSpace > dataLength2)
    jack_ringbuffer_read(_ringbuffer, (char*)data, dataLength2);
}

void MaddeLOIDSynth::setup2()  // rename to setupUI
{
  QAction* madde = new QAction("MaddeLOID", this);
  _ctrl->addPluginAction(madde);
  connect(madde, SIGNAL(triggered()), this, SLOT(madde_triggered()));
}

void MaddeLOIDSynth::madde_triggered() {
  if (_madde) return;

  _madde = new MaddeWindow();
  _madde->setVisible(true);

  connect(_madde, SIGNAL(loadDefault()), this, SLOT(loadDefault()));
  connect(_madde, SIGNAL(loadFile(QString)), this, SLOT(loadFile(QString)));
  connect(_madde, SIGNAL(saveToFile(QString)), this, SLOT(saveToFile(QString)));
  connect(_madde, SIGNAL(mouseEvent(int)), this, SLOT(mouseEvent(int)));

  connect(_madde, SIGNAL(valueChanged(QString, QVariant)), this,
          SLOT(madde_valueChanged(QString, QVariant)));
  connect(_madde, SIGNAL(closed()), this, SLOT(madde_closed()));

  for (int i = 0; i < 6; i++) {
    _madde->setValue("F" + QVariant(i + 1).toString(), init_f[i]);
    _madde->setValue("BW" + QVariant(i + 1).toString(), init_bw[i]);
    _madde->setValue("A" + QVariant(i + 1).toString(), init_gain_db[i]);
    if (i != 5) {
      _madde->setValue("UseF" + QVariant(i + 1).toString(), true);
      _resEnabled[i] = true;
    } else
      _resEnabled[i] = false;
    _resNeedsUpdate[i] = false;
  }

  _madde->setValue("gaindb", init_controls[0]);
  _madde->setValue("slope", init_controls[1]);
  _madde->setValue("slopedepthdb", init_controls[2]);
}

void MaddeLOIDSynth::loadDefault() {
  for (int i = 0; i < 6; i++) {
    _madde->setValue("F" + QVariant(i + 1).toString(), init_f[i]);
    _madde->setValue("BW" + QVariant(i + 1).toString(), init_bw[i]);
    _madde->setValue("A" + QVariant(i + 1).toString(), init_gain_db[i]);
    if (i != 5) {
      _madde->setValue("UseF" + QVariant(i + 1).toString(), true);
      _resEnabled[i] = true;
    } else
      _resEnabled[i] = false;
    _resNeedsUpdate[i] = false;
  }

  // synth
  for (int i = 0; i < RES_COUNT; i++) {
    _res[i].f = init_f[i];
    _res[i].bw = init_bw[i];
    _res[i].gain_db = init_gain_db[i];
    _res[i].enabled = 1;
    EprResonanceUpdate(&_res[i], _osc.fs);
  }
  _res[5].enabled = 0;

  _src.gaindb = init_controls[0];
  _src.slope = init_controls[1];
  _src.slopedepthdb = init_controls[2];

  _madde->setValue("gaindb", init_controls[0]);
  _madde->setValue("slope", init_controls[1]);
  _madde->setValue("slopedepthdb", init_controls[2]);
}

void MaddeLOIDSynth::loadFile(QString fileName) {
  MaddeFile file;
  file.readFromFile(fileName);

  _src.gaindb = file.getEpRSource("gaindb").toDouble();
  _src.slopedepthdb = file.getEpRSource("slopedepthdb").toDouble();
  _src.slope = file.getEpRSource("slope").toDouble();
  _madde->setValue("gaindb", _src.gaindb);
  _madde->setValue("slopedepthdb", _src.slopedepthdb);
  _madde->setValue("slope", _src.slope);

  for (int i = 0; i < 6; i++) {
    float frq = file.getFormant("F" + QVariant(i + 1).toString()).toFloat();
    float q = file.getFormant("Q" + QVariant(i + 1).toString()).toFloat();
    QVariant a = file.getFormant("A" + QVariant(i + 1).toString());
    bool useformant =
        file.getFormant("UseF" + QVariant(i + 1).toString()).toBool();
    float a2 = init_gain_db[i];
    if (a.toString().length() > 0) a2 = a.toFloat();
    _madde->setValue("F" + QVariant(i + 1).toString(), frq);
    _madde->setValue("BW" + QVariant(i + 1).toString(), frq / q);
    _madde->setValue("A" + QVariant(i + 1).toString(), a2);
    if (useformant) {
      _madde->setValue("UseF" + QVariant(i + 1).toString(), true);
      _resEnabled[i] = true;
    } else {
      _resEnabled[i] = false;
      _madde->setValue("UseF" + QVariant(i + 1).toString(), false);
    }

    _res[i].f = frq;
    _res[i].bw = frq / q;
    _res[i].gain_db = a2;
    _res[i].enabled = useformant;
    EprResonanceUpdate(&_res[i], _osc.fs);
  }
}

void MaddeLOIDSynth::madde_valueChanged(QString key, QVariant value) {
  if (key[0] == 'F') {
    key.replace("F", "");
    int index = QVariant(key).toInt() - 1;
    _res[index].f = value.toFloat();
    _resNeedsUpdate[index] = true;
  }
  if (key[0] == 'A') {
    key.replace("A", "");
    int index = QVariant(key).toInt() - 1;
    _res[index].gain_db = value.toFloat();
    _resNeedsUpdate[index] = true;
  }
  if (key[0] == 'Q') {
    key.replace("Q", "");
    int index = QVariant(key).toInt() - 1;
    _res[index].bw = _res[index].f / value.toFloat();
    _resNeedsUpdate[index] = true;
  }
  if (key.startsWith("UseF")) {
    key.replace("UseF", "");
    int index = QVariant(key).toInt() - 1;
    _res[index].enabled = (int)value.toBool();
  }
  if (key == "gaindb") _src.gaindb = value.toFloat();
  if (key == "slope") _src.slope = value.toFloat();
  if (key == "slopedepthdb") _src.slopedepthdb = value.toFloat();
}

void MaddeLOIDSynth::madde_closed() {
  _madde->deleteLater();
  _madde = nullptr;
}

void MaddeLOIDSynth::setup(IController* ctrl) {
  _ctrl = ctrl;

  _osc.fs = ctrl->sampleRate();

  QTimer::singleShot(100, this, SLOT(setup2()));

  // load default values

  for (int i = 0; i < RES_COUNT; i++) {
    _res[i].f = init_f[i];
    _res[i].bw = init_bw[i];
    _res[i].gain_db = init_gain_db[i];
    _res[i].enabled = 1;
    EprResonanceUpdate(&_res[i], _osc.fs);
  }
  _res[5].enabled = 0;

  _src.gaindb = init_controls[0];
  _src.slope = init_controls[1];
  _src.slopedepthdb = init_controls[2];

  _ringbuffer = jack_ringbuffer_create(4096 * sizeof(float));
}

void MaddeLOIDSynth::saveToFile(QString fileName) {
  MaddeFile file;

  for (int i = 0; i < 6; i++) {
    float f = _res[i].f;
    if (f == 0) f = init_f[i];
    float q = f / _res[i].bw;
    if (std::isnan(q)) q = 10;

    file.setFormant("F" + QVariant(i + 1).toString(), f);
    file.setFormant("Q" + QVariant(i + 1).toString(), q);
    file.setFormant("A" + QVariant(i + 1).toString(), _res[i].gain_db);
    file.setFormant("UseF" + QVariant(i + 1).toString(), _res[i].enabled);
  }

  file.setEpRSource("gaindb", _src.gaindb);
  file.setEpRSource("slope", _src.slope);
  file.setEpRSource("slopedepthdb", _src.slopedepthdb);

  file.writeToFile(fileName);
}

void MaddeLOIDSynth::mouseEvent(int status) {
  DEVLOG_INFO("mouse " + STR(status))
  if (status == 1)
    start(48);
  else
    stop();
}
